-
Notifications
You must be signed in to change notification settings - Fork 136
Add WebSocket fallback support #570
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Breaking change: Decoupled keys/certs so CLI can specify multiple without agony. The HTTP stuff is already a breaking change so yolo.
WalkthroughIntroduces WebTransport polyfills and WebSocket fallback across JS and Rust, generalizes transport via traits in Rust, adds HTTP/HTTPS WebSocket support with JWT auth in relay, updates auth/cluster routing, and overhauls config. Also adjusts encoding/decoding utilities, support introspection, build scripts, and dependencies. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor App
participant Hang as Hang.Connection
participant MoqJS as js/moq connect()
participant WT as WebTransport (native/polyfill)
participant WS as WebSocket
App->>Hang: new Connection(url, { websocket? })
Hang->>MoqJS: connect(url, { websocket })
rect rgba(200,230,255,0.3)
note over MoqJS: Race with cancellation and URL heuristics
par WT head-start (~200ms)
MoqJS->>WT: connectWebTransport(url, options)
and WS (if enabled)
MoqJS->>WS: connectWebSocket(url)
end
end
MoqJS-->>Hang: Connection (winner)
Hang-->>App: status connected/failed
sequenceDiagram
autonumber
actor Client
participant Web as Relay Web (Axum)
participant Auth as Auth.verify(path, token)
participant Cluster as Cluster
participant Moq as moq_lite::Session
Client->>Web: HTTP(S)/WS GET /... ?jwt=...
Web->>Auth: verify(path, token)
alt Authorized
Auth-->>Web: AuthToken
Web->>Cluster: publisher(token) / subscriber(token)
Cluster-->>Web: handles (optional)
Web->>Client: Upgrade to WS (if WS)
Web->>Moq: Session::accept(sub, pub)
Mooq->>Web: run until close
Web-->>Client: Close on session end
else Unauthorized
Auth-->>Web: AuthError
Web-->>Client: 401 / close request
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
js/moq/src/stream.ts (1)
388-402: Varint62 encoding off-by-one at boundaries (inflates 63 and 16383).Using < instead of <= encodes 63 and 16383 in the next width (2 bytes and 4 bytes respectively), diverging from QUIC varint sizing.
export function setVint62(dst: ArrayBuffer, v: bigint): Uint8Array { - if (v < MAX_U6) { + if (v <= MAX_U6) { return setUint8(dst, Number(v)); } - if (v < MAX_U14) { + if (v <= MAX_U14) { return setUint16(dst, Number(v) | 0x4000); } if (v <= MAX_U30) { return setUint32(dst, Number(v) | 0x80000000); } //if (v <= MAX_U62) { return setUint64(dst, BigInt(v) | 0xc000000000000000n);Please add tests that round‑trip encode/decode boundary values: 63, 64, 16383, 16384, 1073741823, 1073741824.
js/hang/src/connection.ts (1)
60-93: No reconnect on clean close; status never returns to "disconnected"If the connection closes normally (no exception), we await connection.closed() and then exit without scheduling a retry or updating status. UI can get stuck and reload=true is ignored on clean closes.
Proposed fix:
const connection = await Promise.race([cancel, pending]); @@ effect.set(this.status, "connected", "disconnected"); @@ - await Promise.race([cancel, connection.closed()]); + await Promise.race([cancel, connection.closed()]); + // Transition back to disconnected on clean close and optionally reload. + effect.set(this.status, "disconnected", "connected"); + effect.set(this.status, "disconnected", "connecting"); + effect.set(this.established, undefined); + if (this.reload) { + const tick = this.#tick.peek() + 1; + // Use the base delay after a successful session. + effect.timer(() => this.#tick.set((prev) => Math.max(prev, tick)), this.delay); + } } catch (err) { console.warn("connection error:", err); // Exponential backoff. if (this.reload) { + // Ensure status reflects the failure. + effect.set(this.status, "disconnected", "connected"); + effect.set(this.status, "disconnected", "connecting"); const tick = this.#tick.peek() + 1; effect.timer(() => this.#tick.set((prev) => Math.max(prev, tick)), this.#delay); // Exponential backoff. this.#delay = Math.min(this.#delay * 2, this.maxDelay) as Time.Milli; }js/hang/src/support/element.ts (1)
119-139: Bug: core support still treated as boolean; tri-state not propagated to summary.
support.webtransportis now "full" | "partial" | "none", but#getCoreSupporttreats any truthy string as full, so "none" incorrectly maps to full. Also,#getSummarydoesn’t degrade to "partial" when core is "partial".Apply this diff:
- #getSummary(support: Full, mode: SupportMode): "full" | "partial" | "none" { - const core = this.#getCoreSupport(support); - - if (core === "none" || mode === "core") return core; + #getSummary(support: Full, mode: SupportMode): "full" | "partial" | "none" { + const core = this.#getCoreSupport(support); + if (mode === "core") return core; + if (core === "none") return "none"; @@ - if (watch === "none" || publish === "none") return "none"; - if (watch === "partial" && publish === "partial") return "partial"; - - return "full"; + if (watch === "none" || publish === "none") return "none"; + if (watch === "partial" && publish === "partial") return "partial"; + if (core === "partial") return "partial"; + return "full"; } - #getCoreSupport(support: Full): "full" | "none" { - if (!support.webtransport) return "none"; - return "full"; + #getCoreSupport(support: Full): "full" | "partial" | "none" { + const wt = support.webtransport; + if (wt === "full") return "full"; + if (wt === "partial") return "partial"; + return "none"; }Also applies to: 141-144
js/hang/src/support/index.ts (1)
4-4: RenamePartialalias toSupportLevel
Avoid shadowing TS’s globalPartial<T>. Injs/hang/src/support/index.ts, update:
export type Partial = "full" | "partial" | "none";→export type SupportLevel = "full" | "partial" | "none";- Change all local references (e.g.
Full.webtransport,video.capture) to useSupportLevel.
No external imports of thePartialalias detected.rs/moq/src/session/writer.rs (1)
66-71: Don’t reset on Drop after a successful finish/abort.Unconditional reset can convert a graceful finish into an abort on drop. Track completion and only reset if not finished/aborted.
Apply:
pub(super) struct Writer<S: web_transport_trait::SendStream> { - stream: S, + stream: S, buffer: bytes::BytesMut, + done: bool, } impl<S: web_transport_trait::SendStream> Writer<S> { pub fn new(stream: S) -> Self { Self { stream, buffer: Default::default(), + done: false, } } @@ pub async fn finish(&mut self) -> Result<(), Error> { - self.stream.finish().await.map_err(|e| Error::Transport(e.into()))?; + self.stream.finish().await.map_err(|e| Error::Transport(e.into()))?; + self.done = true; Ok(()) } @@ pub fn abort(&mut self, err: &Error) { self.stream.reset(err.to_code()); + self.done = true; } @@ impl<S: web_transport_trait::SendStream> Drop for Writer<S> { fn drop(&mut self) { - // Unlike the Quinn default, we abort the stream on drop. - self.stream.reset(Error::Cancel.to_code()); + // Abort only if not cleanly finished/aborted. + if !self.done { + self.stream.reset(Error::Cancel.to_code()); + } } }
🧹 Nitpick comments (38)
rs/moq/src/coding/varint.rs (1)
206-214: Minor: prefer bit-shifts over integer pow for clarity and zero-cost constantsShifts read more directly as bit-widths and avoid calling
powat runtime (however cheap). Purely a readability/micro-optimization nit.Apply this diff:
- if self.0 < 2u64.pow(6) { + if self.0 < (1u64 << 6) { w.put_u8(self.0 as u8); - } else if self.0 < 2u64.pow(14) { + } else if self.0 < (1u64 << 14) { w.put_u16((0b01 << 14) | self.0 as u16); - } else if self.0 < 2u64.pow(30) { + } else if self.0 < (1u64 << 30) { w.put_u32((0b10 << 30) | self.0 as u32); } else { w.put_u64((0b11 << 62) | self.0); }js/moq/package.json (1)
23-25: Consider making the WS polyfill optional to keep installs lean.If the polyfill is only dynamically imported on fallback, publishing it as an optional dependency avoids pulling it into environments that never use it.
"dependencies": { - "async-mutex": "^0.5.0", - "@kixelated/web-transport-ws": "^0.1" + "async-mutex": "^0.5.0" }, + "optionalDependencies": { + "@kixelated/web-transport-ws": "^0.1" + },If you do need it always present (e.g., for bundlers without dynamic externals), keep it in dependencies.
rs/moq-relay/cfg/root.toml (2)
15-19: Port selection: TCP 4443 for HTTP may be confusing alongside QUIC on UDP 4443.Sharing the same numeric port across TCP/UDP is valid, but 4443 usually implies TLS. Consider a distinct dev port (e.g., 4444) or add a clarifying comment to avoid operator confusion.
-[web.http] -# Listen for HTTP and WebSocket (TCP) connections on the given address. -# Defaults to disabled if not provided. -listen = "[::]:4443" +[web.http] +# Listen for HTTP and WebSocket (TCP) connections on the given address. +# Defaults to disabled if not provided. +# Note: QUIC uses UDP:4443 above; choose a distinct TCP port for clarity in dev. +listen = "[::]:4444"
15-19: Call out security/mixed‑content expectations for WS.When serving from https origins, ws:// will be blocked as mixed content; operators will need wss://. A brief note here will prevent surprises.
[web.http] # Listen for HTTP and WebSocket (TCP) connections on the given address. # Defaults to disabled if not provided. listen = "[::]:4443" +# +# Tip: Browsers block ws:// from https pages. Use a TLS terminator/reverse proxy +# to expose wss:// in production even if QUIC/TLS is optional.js/tsconfig.json (1)
9-11: Split debug maps into a dev tsconfig instead of commenting out.Source maps and declaration maps are useful during local debugging but can stay off for publish. Consider a tsconfig.dev.json that extends this and enables both.
Example:
- tsconfig.dev.json
- { "extends": "./tsconfig.json", "compilerOptions": { "sourceMap": true, "declarationMap": true } }
- Use it in dev scripts only.
js/moq/src/stream.ts (2)
69-81: Empty chunk handling may be too strict; also avoid an extra copy.Throwing on zero-length chunks could break defensive polyfills; skipping them is safer. Also, wrapping result.value in a new Uint8Array copies data unnecessarily.
- const result = await this.#reader.read(); - if (result.done) { - return false; - } - - if (result.value.byteLength === 0) { - throw new Error("unexpected empty chunk"); - } - - const buffer = new Uint8Array(result.value); + const result = await this.#reader.read(); + if (result.done) return false; + const chunk = result.value; + // Some environments may yield empty chunks; ignore them defensively. + if (chunk.byteLength === 0) return true; + const buffer = chunk;
187-193: Use getUint16 for clarity (unsigned) when decoding 2‑byte varints.Semantics are unsigned; masking fixes it, but using Uint avoids confusion.
- const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength); - return BigInt(view.getInt16(0)) & 0x3fffn; + const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength); + return BigInt(view.getUint16(0)) & 0x3fffn;js/justfile (1)
11-11: Use frozen installs in CI and make it the default via flags.Reproducible installs reduce heisenbugs in CI. You already accept flags—default them to --frozen-lockfile.
-web url='http://localhost:4443/anon': - pnpm i +web url='http://localhost:4443/anon': + pnpm install @@ -check flags="": - pnpm install {{flags}} +check flags="--frozen-lockfile": + pnpm install {{flags}} @@ -fix flags="": +fix flags="--frozen-lockfile": # Fix the JS packages pnpm install {{flags}} @@ -build flags="": - pnpm install {{flags}} +build flags="--frozen-lockfile": + pnpm install {{flags}}Also applies to: 16-16, 33-33, 47-47
rs/moq-relay/cfg/leaf.toml (1)
15-18: Comment/port mismatch: QUIC comment says 4443 but listen/advertise use 4444The new [web.http] TCP listener on 4444 looks fine alongside QUIC on UDP 4444, but the comment above still claims UDP:4443, which is misleading.
Apply one of:
-# Listen for QUIC connections on UDP:4443 +# Listen for QUIC connections on UDP:4444or, if the intent was to keep QUIC on 4443:
-listen = "[::]:4444" +listen = "[::]:4443"Also applies to: 7-9
rs/moq-relay/src/auth.rs (2)
22-32: Return more precise HTTP status codes per auth failureAll AuthError variants currently map to 401. Suggest finer mapping to aid clients and logging:
- UnexpectedToken -> 400 Bad Request
- ExpectedToken, DecodeFailed -> 401 Unauthorized
- IncorrectRoot -> 403 Forbidden
-impl From<AuthError> for http::StatusCode { - fn from(_: AuthError) -> Self { - http::StatusCode::UNAUTHORIZED - } -} +impl From<AuthError> for http::StatusCode { + fn from(err: AuthError) -> Self { + match err { + AuthError::UnexpectedToken => http::StatusCode::BAD_REQUEST, + AuthError::ExpectedToken | AuthError::DecodeFailed => http::StatusCode::UNAUTHORIZED, + AuthError::IncorrectRoot => http::StatusCode::FORBIDDEN, + } + } +} impl axum::response::IntoResponse for AuthError { - fn into_response(self) -> axum::response::Response { - http::StatusCode::UNAUTHORIZED.into_response() - } + fn into_response(self) -> axum::response::Response { + let status: http::StatusCode = self.clone().into(); + // Optional: include a short error string to aid debugging without leaking sensitive details. + (status, self.to_string()).into_response() + } }
95-110: Token/key handling edge: “UnexpectedToken” semanticsReturning UnexpectedToken when a token is provided but no key is configured is sensible; just confirm the web layer doesn’t prompt clients to retry with a token in this case. If you adopt the status code change above, this will become a 400 which matches the situation.
rs/moq-relay/Cargo.toml (1)
17-18: WS feature enablement and deps LGTM; consider minimal-feature axum.The
wsfeature on axum andweb-transport-wsdep fit the WS fallback. Optionally disable axum default features to keep the binary lean.Suggested tweak (optional):
-axum = { version = "0.8", features = ["tokio", "ws"] } +axum = { version = "0.8", default-features = false, features = ["tokio", "ws"] }Also applies to: 20-20, 36-36
js/moq/src/lite/connection.ts (1)
115-124: Always-log finally is good; consider adding a local catch for decode errors for better context.Right now any decode error surfaces only as a generic "fatal error running connection" from #run(). A localized error makes triage easier without changing behavior.
Apply:
async #runSession() { - try { + try { // Receive messages until the connection is closed. for (;;) { const msg = await SessionInfo.decodeMaybe(this.#session.reader); if (!msg) break; // TODO use the session info } + } catch (err) { + if (!this.#closed) { + console.error("error reading session info", err); + } } finally { console.warn("session stream closed"); } }rs/moq-relay/cfg/dev.toml (1)
18-23: Bind HTTP to loopback by default in dev to avoid accidental exposureListening on all interfaces in dev can leak the fingerprint/track endpoints on shared machines. Prefer loopback.
Apply:
[web.http] # Listen for HTTP and WebSocket connections on the given TCP address. # This is unfortunately required to serve certificate.sha256 for local development. # However, as a bonus, we can serve tracks via both HTTP and WebSocket fallbacks. -listen = "[::]:4443" +listen = "127.0.0.1:4443"rs/moq-relay/cfg/prod.toml (3)
15-18: Document operational caveats for optional HTTP on :80If enabled, binding :80 on Linux requires root or CAP_NET_BIND_SERVICE; behind a TLS terminator you likely don’t want a cleartext listener. Add a note to prevent misconfig.
19-27: Avoid TLS config duplication; ensure fail-fast if certs are missingYou repeat cert/key under both [server.tls] and [web.https]. Easy to drift. Consider a single tls.* block referenced by both, or at least add a comment stating they must be identical. Also ensure startup fails fast if either file is unreadable.
4-8: Same port (443) on UDP and TCP is OK; add a hint about firewall rulesUsing UDP:443 for QUIC and TCP:443 for HTTPS is correct; add a brief note reminding operators to open both protocols in security groups/firewalls.
rs/moq-relay/src/main.rs (1)
28-36: Fail fast on web bind errors instead of panicking inside a taskIf web.run() errors (e.g., EADDRINUSE), the panic happens in a spawned task and the process may keep serving QUIC, which is confusing. Prefer surfacing bind errors before entering the accept loop.
Example pattern (outside this hunk):
// Bind or validate listeners first so errors are synchronous. let web = Web::new(state, config.web); // If Web exposes a bind/prepare step, call it here and handle errors. // web.prepare().context("failed to prepare web listeners")?; tokio::spawn(async move { if let Err(err) = web.run().await { tracing::error!(%err, "web server exited"); // Optionally: std::process::exit(1); } });Would you like me to propose a minimal prepare() API in rs/moq-relay/src/web.rs?
rs/moq-relay/src/config.rs (1)
33-37: Add inline config tests for CLI-over-file precedence and defaultsGiven serde(flatten) + deny_unknown_fields, add small unit tests here to lock behavior.
You can append:
#[cfg(test)] mod tests { use super::*; use clap::Parser; #[test] fn cli_overrides_file() { // Simulate: file sets web.http.listen=:80, CLI sets :8080 let mut cfg: Config = toml::from_str(r#" [web.http] listen = "[::]:80" "#).unwrap(); // Emulate CLI args cfg.update_from(["bin", "--web.http.listen", "[::]:8080"]); assert_eq!(cfg.web.http.as_ref().unwrap().listen.unwrap().to_string(), "[::]:8080"); } #[test] fn default_web_is_disabled() { let cfg = Config::parse_from(["bin"]); assert!(cfg.web.http.is_none() && cfg.web.https.is_none()); } }rs/moq-relay/src/cluster.rs (2)
71-74: Comment wording nit: “publish” vs “subscribe”.
This block is for subscribing; the comment says “publish our primary broadcasts.”Apply:
- “If this is a cluster node, then only serve our primary broadcasts.”
- “Otherwise serve everything.”
69-95: Add inline tests for token scoping.
Cover: cluster=true vs false, valid/invalid roots, subscribe/publish filters.I can add a minimal #[cfg(test)] block validating these branches.
rs/moq-relay/src/connection.rs (2)
15-23: Prefer Authorization header over URL query for JWTsJWTs in query params are commonly logged by proxies and appear in referrers. Support Authorization: Bearer ... and fall back to query only if necessary.
38-49: Tidy duplicate “accepted” logsYou log “session accepted” twice (Line 33 and match arms). With the prior change, keep the detailed per-role logs here and drop the earlier one.
rs/moq-native/src/server.rs (2)
164-168: Surface accept errors instead of silently droppingCurrently, Errs from accept_session are ignored. Log them so operators can see handshake/ALPN issues.
- Some(res) = self.accept.next() => { - if let Ok(session) = res { - return Some(session) - } - } + Some(res) = self.accept.next() => { + match res { + Ok(session) => return Some(session), + Err(e) => tracing::warn!(error = ?e, "failed to accept session"), + } + }
228-229: Typo in comment“cooresponding” → “corresponding”.
- // Load a certificate and cooresponding key from a file + // Load a certificate and corresponding key from a filers/moq/src/session/reader.rs (2)
20-28: Remove commented-out accept() or gate with cfg(test)Dead/commented code adds noise. Either delete or wrap a minimal test helper behind cfg(test).
- /* - pub async fn accept<S: transport::Session>(session: &S) -> Result<Self, Error> - where - S::RecvStream: T, - { - let stream = session.accept_uni().await?; - Ok(Self::new(stream)) - } - */
81-93: closed() semantics are correct; consider a unit testBehavior (error on unexpected trailing bytes) is precise. Add a small inline test to lock this in.
rs/moq/src/error.rs (1)
88-89: MSRV safety: avoid io::Error::other (use new + Other).If you keep the manual Clone fallback, Error::other requires newer Rust. Prefer:
- std::io::Error::other("Transport error (cloned)") + std::io::Error::new(std::io::ErrorKind::Other, "Transport error (cloned)")rs/moq/src/session/writer.rs (1)
17-29: Remove stale commented code.Dead/commented API adds noise. Drop or move behind a feature gate.
rs/moq/src/session/subscriber.rs (2)
56-59: Don’t swallow uni stream errors; log them.Currently errors from run_uni_stream() are dropped. Emit a debug log to aid triage.
- web_async::spawn(async move { - this.run_uni_stream(stream).await.ok(); - }); + web_async::spawn(async move { + if let Err(err) = this.run_uni_stream(stream).await { + tracing::debug!(%err, "uni stream error"); + } + });
127-143: Broadcasts map is never populated; remove or insert on announce.self.broadcasts is only removed from (Line 171) but never inserted, so the map is effectively unused. Either remove the field or insert entries when starting a broadcast.
// Run the broadcast in the background until all consumers are dropped. self.origin .as_mut() .unwrap() .publish_broadcast(path.clone(), broadcast.consumer); - web_async::spawn(self.clone().run_broadcast(path, broadcast.producer)); + // Track for lookup/cleanup. + self.broadcasts.lock().insert(path.clone(), broadcast.producer.clone()); + + web_async::spawn(self.clone().run_broadcast(path, broadcast.producer));rs/moq-relay/src/web.rs (1)
162-169: Typo: “Unfortuantely” → “Unfortunately”.- // Unfortuantely, we need to convert from Axum to Tungstenite. + // Unfortunately, we need to convert from Axum to Tungstenite.rs/moq/src/session/mod.rs (6)
20-22: Maketransportprivate to preserve invariants and allow future changes.Exposing the transport directly couples external callers to the concrete transport API and makes it harder to evolve. Prefer a private field with an accessor.
Apply this diff:
-pub struct Session<S: web_transport_trait::Session> { - pub transport: S, -} +pub struct Session<S: web_transport_trait::Session> { + transport: S, +}And add a lightweight accessor inside this impl block:
pub fn transport(&self) -> &S { &self.transport }
42-63: HandleError::Cancelas a normal shutdown to avoid warning noise and confusing close codes.
run_session()returnsError::Cancelon graceful end-of-stream. The current match treats it as an error (warn, non-zero close code viaerr.to_code()), which is misleading.Apply this diff to add a dedicated branch before the generic
Err(err)arm:match res { - Err(Error::Transport(_)) => { + Err(Error::Transport(_)) => { tracing::info!("session terminated"); session.close(1, ""); - } + } + Err(Error::Cancel) => { + tracing::info!("session canceled"); + session.close(0, ""); + } Err(err) => { tracing::warn!(%err, "session error"); session.close(err.to_code(), err.to_string().as_ref()); }Also, confirm
close()is idempotent across concurrent callers (user callingSession::close()vs. the background task). If idempotency is not guaranteed by the transport, consider guarding with an atomic once flag.
69-69: Return a more specific error thanCancelon init failure.When the announce init oneshot fails, mapping to
Error::Cancelhides the cause (e.g., early stream close vs. setup error). Prefer a dedicated handshake/init error.Example:
- Introduce
Error::Handshake(&'static str)orError::InitFailed.- Use it here to aid diagnostics and metrics.
I can wire this through
error.rsif you want.
143-146: Consider treating end-of-session as success to simplify callers.Right now a clean end-of-stream yields
Error::Cancel. If you adopt the dedicated match arm above, you can keep this; alternatively, returningOk(())avoids propagating a pseudo-error.Alternative diff if you prefer the latter:
-while let Some(_info) = stream.reader.decode_maybe::<message::SessionInfo>().await? {} -Err(Error::Cancel) +while let Some(_info) = stream.reader.decode_maybe::<message::SessionInfo>().await? {} +Ok(())
149-156: Minor: document close/closed semantics and reason lifetimes.
close(self, err)consumesselfand passeserr.to_string().as_ref(); this borrows a temporary, which is fine ifclosedoesn't retain it. Add a brief doc note to clarify that the reason is used synchronously and not retained.
1-16: Add inline unit tests under#[cfg(test)]per repo guidelines.Given the new generic transport, add tests using a mock
Sto validate:
- Successful connect/accept negotiation.
- Version mismatch path.
Error::Cancelhandling and close code mapping.I can scaffold a
#[cfg(test)]module with a minimal mock implementingweb_transport_trait::Sessionto exerciseconnect_setup/accept_setup.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (2)
js/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlrs/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (38)
js/hang/package.json(0 hunks)js/hang/src/connection.ts(1 hunks)js/hang/src/support/element.ts(1 hunks)js/hang/src/support/index.ts(2 hunks)js/justfile(3 hunks)js/moq/package.json(1 hunks)js/moq/src/connection.ts(3 hunks)js/moq/src/lite/connection.ts(1 hunks)js/moq/src/stream.ts(1 hunks)js/tsconfig.json(1 hunks)rs/.cargo/config.toml(0 hunks)rs/Cargo.toml(1 hunks)rs/hang-cli/src/server.rs(2 hunks)rs/moq-native/Cargo.toml(2 hunks)rs/moq-native/src/client.rs(2 hunks)rs/moq-native/src/lib.rs(1 hunks)rs/moq-native/src/server.rs(4 hunks)rs/moq-relay/Cargo.toml(2 hunks)rs/moq-relay/cfg/dev.toml(1 hunks)rs/moq-relay/cfg/leaf.toml(1 hunks)rs/moq-relay/cfg/prod.toml(1 hunks)rs/moq-relay/cfg/root.toml(1 hunks)rs/moq-relay/src/auth.rs(21 hunks)rs/moq-relay/src/cluster.rs(2 hunks)rs/moq-relay/src/config.rs(2 hunks)rs/moq-relay/src/connection.rs(1 hunks)rs/moq-relay/src/main.rs(1 hunks)rs/moq-relay/src/web.rs(4 hunks)rs/moq/Cargo.toml(1 hunks)rs/moq/src/coding/varint.rs(1 hunks)rs/moq/src/error.rs(3 hunks)rs/moq/src/lib.rs(1 hunks)rs/moq/src/session/mod.rs(6 hunks)rs/moq/src/session/publisher.rs(14 hunks)rs/moq/src/session/reader.rs(3 hunks)rs/moq/src/session/stream.rs(2 hunks)rs/moq/src/session/subscriber.rs(13 hunks)rs/moq/src/session/writer.rs(2 hunks)
💤 Files with no reviewable changes (2)
- js/hang/package.json
- rs/.cargo/config.toml
🧰 Additional context used
📓 Path-based instructions (6)
rs/moq-relay/**
📄 CodeRabbit inference engine (CLAUDE.md)
The CDN/relay (rs/moq-relay) must not include application logic, media codecs, or track details
Files:
rs/moq-relay/cfg/prod.tomlrs/moq-relay/cfg/root.tomlrs/moq-relay/src/config.rsrs/moq-relay/cfg/leaf.tomlrs/moq-relay/cfg/dev.tomlrs/moq-relay/Cargo.tomlrs/moq-relay/src/cluster.rsrs/moq-relay/src/auth.rsrs/moq-relay/src/connection.rsrs/moq-relay/src/main.rsrs/moq-relay/src/web.rs
rs/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place Rust crates under the rs/ directory
Files:
rs/moq-relay/cfg/prod.tomlrs/moq/Cargo.tomlrs/moq-relay/cfg/root.tomlrs/moq-native/src/lib.rsrs/moq/src/lib.rsrs/moq-relay/src/config.rsrs/moq-relay/cfg/leaf.tomlrs/Cargo.tomlrs/moq/src/coding/varint.rsrs/moq-relay/cfg/dev.tomlrs/moq-native/src/client.rsrs/moq-relay/Cargo.tomlrs/moq-relay/src/cluster.rsrs/moq-native/Cargo.tomlrs/hang-cli/src/server.rsrs/moq/src/error.rsrs/moq/src/session/stream.rsrs/moq-relay/src/auth.rsrs/moq-relay/src/connection.rsrs/moq-relay/src/main.rsrs/moq-relay/src/web.rsrs/moq-native/src/server.rsrs/moq/src/session/reader.rsrs/moq/src/session/subscriber.rsrs/moq/src/session/writer.rsrs/moq/src/session/mod.rsrs/moq/src/session/publisher.rs
js/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place JS/TS packages under the js/ directory
Files:
js/moq/package.jsonjs/moq/src/lite/connection.tsjs/hang/src/support/element.tsjs/moq/src/stream.tsjs/hang/src/connection.tsjs/moq/src/connection.tsjs/tsconfig.jsonjs/justfilejs/hang/src/support/index.ts
rs/**/src/**/*.rs
📄 CodeRabbit inference engine (CLAUDE.md)
Rust tests should be integrated within source files (inline unit tests)
Files:
rs/moq-native/src/lib.rsrs/moq/src/lib.rsrs/moq-relay/src/config.rsrs/moq/src/coding/varint.rsrs/moq-native/src/client.rsrs/moq-relay/src/cluster.rsrs/hang-cli/src/server.rsrs/moq/src/error.rsrs/moq/src/session/stream.rsrs/moq-relay/src/auth.rsrs/moq-relay/src/connection.rsrs/moq-relay/src/main.rsrs/moq-relay/src/web.rsrs/moq-native/src/server.rsrs/moq/src/session/reader.rsrs/moq/src/session/subscriber.rsrs/moq/src/session/writer.rsrs/moq/src/session/mod.rsrs/moq/src/session/publisher.rs
{rs,js}/hang/**
📄 CodeRabbit inference engine (CLAUDE.md)
All media logic belongs in the hang layer (rs/hang and js/hang)
Files:
js/hang/src/support/element.tsjs/hang/src/connection.tsjs/hang/src/support/index.ts
rs/Cargo.toml
📄 CodeRabbit inference engine (CLAUDE.md)
Maintain the Rust workspace configuration in rs/Cargo.toml
Files:
rs/Cargo.toml
🧠 Learnings (1)
📚 Learning: 2025-08-13T21:30:35.001Z
Learnt from: CR
PR: kixelated/moq#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-13T21:30:35.001Z
Learning: Applies to rs/Cargo.toml : Maintain the Rust workspace configuration in rs/Cargo.toml
Applied to files:
rs/Cargo.toml
🧬 Code graph analysis (13)
js/moq/src/lite/connection.ts (2)
js/moq/src/ietf/connection.ts (2)
msg(125-190)msg(196-201)js/moq/src/lite/session.ts (1)
SessionInfo(136-164)
js/moq/src/connection.ts (1)
js/hang/src/publish/element.ts (2)
url(92-94)url(96-98)
rs/moq/src/error.rs (1)
rs/moq/src/session/mod.rs (1)
new(25-72)
rs/moq/src/session/stream.rs (1)
rs/moq/src/session/mod.rs (1)
accept(101-112)
rs/moq-relay/src/auth.rs (3)
rs/moq/src/path.rs (10)
from(233-235)from(239-242)from(252-272)new(82-98)as_path(13-13)as_path(17-19)as_path(23-26)as_path(30-32)as_path(36-38)as_path(42-44)rs/moq-relay/src/web.rs (4)
into_response(294-296)into_response(329-331)new(89-91)new(273-275)rs/moq/src/model/origin.rs (7)
root(285-310)root(425-427)root(515-517)new(17-19)new(62-67)new(112-118)new(451-470)
rs/moq-relay/src/connection.rs (1)
rs/moq/src/model/origin.rs (5)
root(285-310)root(425-427)root(515-517)allowed(429-431)allowed(519-521)
rs/moq-relay/src/main.rs (3)
rs/moq-relay/src/web.rs (2)
new(89-91)new(273-275)rs/moq-relay/src/cluster.rs (1)
new(58-67)rs/moq-relay/src/auth.rs (1)
new(70-88)
rs/moq-relay/src/web.rs (5)
rs/moq-relay/src/cluster.rs (3)
get(96-101)new(58-67)run(103-150)rs/moq-native/src/server.rs (3)
new(86-146)accept(157-178)close(217-219)rs/moq/src/session/mod.rs (3)
new(25-72)accept(101-112)close(149-151)rs/moq-relay/src/auth.rs (1)
new(70-88)rs/moq-relay/src/connection.rs (1)
run(14-61)
rs/moq/src/session/reader.rs (2)
rs/moq/src/session/mod.rs (3)
new(25-72)stream(144-144)closed(154-156)rs/moq/src/session/writer.rs (2)
new(10-15)closed(60-63)
rs/moq/src/session/subscriber.rs (3)
rs/moq/src/session/reader.rs (1)
new(13-18)rs/moq/src/session/writer.rs (1)
new(10-15)rs/moq/src/session/stream.rs (1)
open(10-18)
rs/moq/src/session/writer.rs (3)
rs/moq/src/session/mod.rs (2)
new(25-72)stream(144-144)rs/moq/src/session/reader.rs (1)
new(13-18)rs/moq/src/coding/encode.rs (11)
encode(6-6)encode(10-12)encode(16-18)encode(22-25)encode(29-32)encode(36-41)encode(45-50)encode(54-57)encode(61-64)encode(68-70)encode(74-77)
rs/moq/src/session/mod.rs (4)
rs/moq/src/session/publisher.rs (3)
new(16-20)stream(48-48)stream(136-136)rs/moq/src/session/reader.rs (2)
new(13-18)closed(80-93)rs/moq/src/session/writer.rs (2)
new(10-15)closed(60-63)rs/moq/src/session/stream.rs (2)
open(10-18)accept(20-27)
rs/moq/src/session/publisher.rs (3)
rs/moq/src/session/mod.rs (3)
new(25-72)stream(144-144)accept(101-112)rs/moq/src/session/writer.rs (2)
new(10-15)finish(51-54)rs/moq/src/session/stream.rs (1)
accept(20-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Check
🔇 Additional comments (34)
rs/moq/src/coding/varint.rs (1)
21-21: Adding Debug to VarInt is a safe, helpful diagnostic enhancement — LGTMThis is an additive trait impl on a public type, non-breaking, and improves logging/printf-debugging without behavior changes.
js/hang/src/connection.ts (1)
22-22: Removal of “unsupported” from ConnectionStatus is safe
Search across TS/TSX shows no callers referencing “unsupported” for ConnectionStatus in js/hang; safe to merge.rs/moq-relay/src/auth.rs (2)
116-121: Root-prefix check looks correct with normalized pathsUsing Path::new(path) and strip_prefix(claims.root) enforces that the requested path lies under the token’s root. Normalization in moq::Path handles leading/trailing and duplicate slashes. LGTM.
123-147: Permission reduction logic is sound; preserves/strips as intendedReducing subscribe/publish by the derived suffix correctly narrows permissions when connecting deeper than the token root. Tests cover empty, specific, and nested cases. LGTM.
rs/moq/Cargo.toml (1)
32-32: Switch to trait abstraction looks good.Using
web-transport-traitat the core crate level aligns with the transport split and enables multiple backends. No issues spotted here.js/hang/src/support/element.ts (1)
288-288: Row change aligns UI with tri-state WebTransport.Good switch to
partial(support.webtransport)to reflect Full/Partial/None in the grid.rs/moq-native/Cargo.toml (1)
36-36: Quinn backend wiring is correct.
web-transport-quinnmatches the workspace split and downstream imports. No issues here.rs/moq/src/lib.rs (1)
24-24: Verify removal ofmoq[_lite]::web_transportand update docs
- Local grep shows no internal references; manually confirm no downstream crates on crates.io rely on
moq[_lite]::web_transport.- Add migration notes to CHANGELOG/RELEASE_NOTES detailing the split into
web-transport-{quinn,ws,trait}.rs/moq-native/src/client.rs (1)
170-175: ALPN selection for HTTPS looks correct.Mapping
"https"→web_transport_quinn::ALPNand"moql"→moq_lite::ALPNis appropriate.rs/moq-relay/src/main.rs (1)
28-36: Good separation of Web state vs. configMoving to Web::new(WebState, WebConfig) improves testability and config clarity. No issues.
rs/Cargo.toml (1)
23-25: No staleweb-transportdependencies remain. All references have been updated to the newweb-transport-quinn,web-transport-trait, andweb-transport-wscrates.rs/moq-relay/src/config.rs (1)
4-4: Import of WebConfig is correct and localizedKeeps Web concerns within moq-relay; conforms to workspace structure.
rs/hang-cli/src/server.rs (2)
7-7: Import path update looks correct.
New path aligns with moq-native’s re-exports.
69-69: Session type swap is consistent with the import.
Signature matches the new Request type.js/moq/src/connection.ts (1)
41-44: Confirm polyfill URL expectations
Please share the @kixelated/web-transport-ws npm/GitHub link or paste its README/constructor implementation so we can verify whether it accepts http(s) and auto-converts to ws(s), or if callers must supply ws(s) URLs.rs/moq-relay/src/connection.rs (3)
51-53: Good: defer handshake until after authAccepting the WebTransport request only after verify() prevents unauthenticated sessions from establishing. Nice.
54-58: Relay stays free of app/codec specificsThis file manages auth/route/session wiring without embedding application logic, media codecs, or track details—aligned with rs/moq-relay guidelines.
59-61: Confirm the contract for “normal” session closurerun() always returns Err(session.closed().await.into()). If clean shutdowns are expected to be non-errors at the call site, consider mapping a normal close to Ok(()).
rs/moq/src/session/reader.rs (3)
7-14: Generic Reader over RecvStream looks goodType generalization aligns with the transport trait and keeps the constructor clean.
40-46: Good: unify transport errorsMapping read_buf errors into Error::Transport ensures consistent error handling across transports.
73-77: Verify external RecvStream::read_chunk signature
Confirm that the importedRecvStream::read_chunk(max: usize)indeed returnsResult<Option<Bytes>, _>with no extra metadata or offset; if it returns a different type, adapt its output toOption<Bytes>before mapping into yourError::Transport.rs/moq/src/session/stream.rs (3)
4-7: Good move: generic Stream over transport session.Struct generics and associated stream types are clean and align with the trait-based refactor.
11-18: ControlType write on open looks correct.Encoding the control type immediately on bi-open is consistent with accept-side decode in session/mod.rs.
10-12: Confirm error conversion assumptions for Session::open_bi()/accept_bi()
Ensure the error type returned byweb_transport_trait::Sessionmethods implementsInto<Box<dyn std::error::Error + Send + Sync>>. If it doesn’t, replace:-.map_err(|err| Error::Transport(err.into()))? +.map_err(|err| Error::Transport(Box::new(err)))?rs/moq/src/error.rs (1)
62-83: Wire code mapping looks consistent.Transport mapped to code 4 aligns with the session close logic.
rs/moq/src/session/writer.rs (1)
31-43: encode path looks solid.Buffer reuse + write loop is efficient and straightforward.
rs/moq/src/session/subscriber.rs (1)
286-302: Frame read/size accounting looks solid.Correctly handles EOF/size mismatch and avoids underflow with checked_sub; trace logs are helpful.
rs/moq-relay/src/web.rs (1)
142-174: Confirm path format passed to auth.verify().Previously we passed request.url().path() (leading “/”). Now the path extractor yields no leading slash. If verify expects a leading slash, token checks may fail. Consider normalizing (format!("/{}", path)) or updating verify().
Would you like me to scan the repo to confirm auth.verify’s expected path format and update the call sites accordingly?
rs/moq/src/session/publisher.rs (2)
276-327: Streaming group writer is correct and resilient.Good use of open_uni + priority, cancellation via stream.closed(), chunked writes, and finish().
335-358: Priority mapping and tests look good.The 24-bit packing and test coverage are clear and sufficient.
rs/moq/src/session/mod.rs (4)
74-84: LGTM: client connect path moves to generic transport and immutable session borrow.The switch to
Stream::open(&session, ...)and the genericSlooks clean; setup ordering and error propagation are preserved.
100-112: LGTM: server accept path with genericSand inferredControlType.Accepting via
Stream::accept(&session)and deferring kind validation toaccept_setupkeeps concerns separated.
114-140: Version gating and backward-compatibility look correct.
- Properly rejects unsupported versions.
- Sends
ServerCompatpreface whenClientCompatis used, thenServerSetup.
24-38: Add explicitClonebound tonew()
session.clone()is called multiple times inSession::new, so requireS: Cloneon the method signature. Optionally, ifweb_async::spawnis an alias fortokio::spawn, you’ll also needS: Send + 'static—please verify and adjust accordingly.Apply:
impl<S: web_transport_trait::Session + Sync> Session<S> { - async fn new( + async fn new( session: S, stream: Stream<S>, publish: Option<OriginConsumer>, subscribe: Option<OriginProducer>, -) -> Result<Self, Error> { +) -> Result<Self, Error> + where + S: Clone, + {
js/moq/src/connection.ts
Outdated
| // Fetch the fingerprint from the server. | ||
| const fingerprint = await fetch(fingerprintUrl); | ||
| const fingerprintText = await fingerprint.text(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Harden fingerprint fetch: check status and trim whitespace.
Avoids opaque failures on 404/HTML and handles trailing newlines.
Apply this diff:
- // Fetch the fingerprint from the server.
- const fingerprint = await fetch(fingerprintUrl);
- const fingerprintText = await fingerprint.text();
+ // Fetch the fingerprint from the server.
+ const res = await fetch(fingerprintUrl);
+ if (!res.ok) {
+ throw new Error(`fingerprint fetch failed: ${res.status} ${res.statusText}`);
+ }
+ const fingerprintText = (await res.text()).trim();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Fetch the fingerprint from the server. | |
| const fingerprint = await fetch(fingerprintUrl); | |
| const fingerprintText = await fingerprint.text(); | |
| // Fetch the fingerprint from the server. | |
| - const fingerprint = await fetch(fingerprintUrl); | |
| const res = await fetch(fingerprintUrl); | |
| if (!res.ok) { | |
| throw new Error(`fingerprint fetch failed: ${res.status} ${res.statusText}`); | |
| } | |
| const fingerprintText = (await res.text()).trim(); |
🤖 Prompt for AI Agents
In js/moq/src/connection.ts around lines 56 to 59, the code fetches the
fingerprint but doesn't validate the HTTP status or trim whitespace; update the
fetch to check response.ok (or status === 200) and if not, read the response
text and throw an error including the HTTP status and response body to avoid
opaque failures on 404/HTML, then call .text(), trim the result (e.g.
fingerprintText.trim()) and use the trimmed fingerprint; ensure any thrown
errors include context (URL and status) for easier debugging.
| let session = match url.scheme() { | ||
| "https" => web_transport::quinn::Session::connect(connection, url).await?, | ||
| moq_lite::ALPN => web_transport::quinn::Session::raw(connection, url), | ||
| "https" => web_transport_quinn::Session::connect(connection, url).await?, | ||
| moq_lite::ALPN => web_transport_quinn::Session::raw(connection, url), | ||
| _ => unreachable!(), | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Matching url.scheme() against moq_lite::ALPN will never hit; use "moql".
This makes the "moql" path unreachable and will unreachable!() at runtime.
Fix:
- let session = match url.scheme() {
- "https" => web_transport_quinn::Session::connect(connection, url).await?,
- moq_lite::ALPN => web_transport_quinn::Session::raw(connection, url),
- _ => unreachable!(),
- };
+ let session = match url.scheme() {
+ "https" => web_transport_quinn::Session::connect(connection, url).await?,
+ "moql" => web_transport_quinn::Session::raw(connection, url),
+ _ => unreachable!(),
+ };Optional: derive both ALPN and session constructor from a single parsed enum of the scheme to avoid future drift between the two matches.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let session = match url.scheme() { | |
| "https" => web_transport::quinn::Session::connect(connection, url).await?, | |
| moq_lite::ALPN => web_transport::quinn::Session::raw(connection, url), | |
| "https" => web_transport_quinn::Session::connect(connection, url).await?, | |
| moq_lite::ALPN => web_transport_quinn::Session::raw(connection, url), | |
| _ => unreachable!(), | |
| }; | |
| let session = match url.scheme() { | |
| "https" => web_transport_quinn::Session::connect(connection, url).await?, | |
| "moql" => web_transport_quinn::Session::raw(connection, url), | |
| _ => unreachable!(), | |
| }; |
🤖 Prompt for AI Agents
In rs/moq-native/src/client.rs around lines 189 to 193, the match arm compares
url.scheme() to moq_lite::ALPN which is the ALPN identifier (not the URL scheme)
so the "moql" path is never matched and the code can hit unreachable!(); change
the match to compare against the actual scheme string "moql" (i.e. use "moql" =>
web_transport_quinn::Session::raw(...)) so the correct branch is taken;
optionally refactor to parse the URL scheme into a small enum and derive both
the ALPN and the session constructor from that enum to keep them in sync.
| // Re-export these crates. | ||
| pub use moq_lite; | ||
| pub use web_transport; | ||
| pub use web_transport_quinn; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Avoid breaking change: keep old re-export alias.
Replacing pub use web_transport; with pub use web_transport_quinn; breaks external users importing moq_native::web_transport. Either add a compatibility alias or bump the crate version.
Apply this diff to preserve the old path:
pub use moq_lite;
-pub use web_transport_quinn;
+pub use web_transport_quinn;
+// Back-compat alias for external crates that used `moq_native::web_transport`
+pub use web_transport_quinn as web_transport;If you prefer not to alias, please bump moq-native to 0.8.0.
🏁 Script executed:
#!/bin/bash
# Search for any existing re-export of `web_transport` and inspect lib.rs
rg -n "pub use web_transport" -g "*.rs" || true
sed -n '1,20p' rs/moq-native/src/lib.rs || trueLength of output: 313
Preserve backward compatibility for moq_native::web_transport
In rs/moq-native/src/lib.rs after the pub use web_transport_quinn; re-export, add:
pub use moq_lite;
pub use web_transport_quinn;
+// Back-compat alias for external crates using `moq_native::web_transport`
+pub use web_transport_quinn as web_transport;Alternatively, bump the moq-native crate version to 0.8.0.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pub use web_transport_quinn; | |
| pub use moq_lite; | |
| pub use web_transport_quinn; | |
| // Back-compat alias for external crates using `moq_native::web_transport` | |
| pub use web_transport_quinn as web_transport; |
🤖 Prompt for AI Agents
In rs/moq-native/src/lib.rs around line 11, maintain backward compatibility for
moq_native::web_transport by adding a re-export alias for the renamed
dependency; specifically add a public re-export that aliases web_transport_quinn
to the old module name (i.e., re-export web_transport_quinn as web_transport) so
existing callers continue to resolve moq_native::web_transport, or alternatively
increment the moq-native crate version to 0.8.0 if you intend to break
compatibility.
| let token = state.auth.verify(&path, params.jwt.as_deref())?; | ||
| let publish = state.cluster.publisher(&token); | ||
| let subscribe = state.cluster.subscriber(&token); | ||
|
|
||
| if publish.is_none() && subscribe.is_none() { | ||
| // Bad token, we can't publish or subscribe. | ||
| return Err(StatusCode::UNAUTHORIZED.into()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Map auth failures to 401 instead of propagating internal errors.
Explicitly convert verification errors to UNAUTHORIZED for WS, /announced, and /fetch.
- let token = state.auth.verify(&path, params.jwt.as_deref())?;
+ let token = state
+ .auth
+ .verify(&path, params.jwt.as_deref())
+ .map_err(|_| StatusCode::UNAUTHORIZED)?;
@@
- let token = state.auth.verify(&prefix, params.jwt.as_deref())?;
+ let token = state
+ .auth
+ .verify(&prefix, params.jwt.as_deref())
+ .map_err(|_| StatusCode::UNAUTHORIZED)?;
@@
- let token = state.auth.verify(&broadcast, params.jwt.as_deref())?;
+ let token = state
+ .auth
+ .verify(&broadcast, params.jwt.as_deref())
+ .map_err(|_| StatusCode::UNAUTHORIZED)?;Also applies to: 208-212, 242-246
🤖 Prompt for AI Agents
In rs/moq-relay/src/web.rs around lines 150-156 (and similarly at 208-212,
242-246), the auth verification call can return internal errors that are
currently propagated; instead, catch verification failures and map them to
StatusCode::UNAUTHORIZED so clients receive 401 rather than internal errors.
Change the code to handle the Result from state.auth.verify by matching or using
map_err to convert any verification error into an UNAUTHORIZED error (returning
Err(StatusCode::UNAUTHORIZED.into())), then proceed to check publish/subscribe
as before; apply the same pattern at the other indicated line ranges (WS,
/announced, /fetch).
| let mut path: Vec<&str> = path.split("/").collect(); | ||
| if path.len() < 2 { | ||
| let track = path.pop().unwrap().to_string(); | ||
|
|
||
| // We need at least a broadcast and a track. | ||
| if path.is_empty() { | ||
| return Err(StatusCode::BAD_REQUEST.into()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Path parsing can panic on empty or single-segment paths.
path.pop().unwrap() will panic. Also handle trailing slashes.
- // The path containts a broadcast/track
- let mut path: Vec<&str> = path.split("/").collect();
- let track = path.pop().unwrap().to_string();
+ // The path contains a broadcast/track; tolerate extra slashes.
+ let mut path: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
+ let Some(track) = path.pop().map(|s| s.to_string()) else {
+ return Err(StatusCode::BAD_REQUEST.into());
+ };
// We need at least a broadcast and a track.
if path.is_empty() {
return Err(StatusCode::BAD_REQUEST.into());
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let mut path: Vec<&str> = path.split("/").collect(); | |
| if path.len() < 2 { | |
| let track = path.pop().unwrap().to_string(); | |
| // We need at least a broadcast and a track. | |
| if path.is_empty() { | |
| return Err(StatusCode::BAD_REQUEST.into()); | |
| // The path contains a broadcast/track; tolerate extra slashes. | |
| let mut path: Vec<&str> = path | |
| .split('/') | |
| .filter(|s| !s.is_empty()) | |
| .collect(); | |
| let Some(track) = path.pop().map(|s| s.to_string()) else { | |
| return Err(StatusCode::BAD_REQUEST.into()); | |
| }; | |
| // We need at least a broadcast and a track. | |
| if path.is_empty() { | |
| return Err(StatusCode::BAD_REQUEST.into()); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will split always return at least one result, or can it return an empty vec?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kixelated str::split() always returns at least one element, even for empty strings:
"".split('/')→[""](one empty string)"abc".split('/')→["abc"](one element)"/".split('/')→["", ""](two empty strings)"a/b".split('/')→["a", "b"](two elements)
So path.pop().unwrap() would never panic from an empty vec after split(). However, it could still be problematic with paths like "/" or "//" where you get only empty segments. That's why the .filter(|s| !s.is_empty()) in my suggestion helps handle trailing/leading slashes and multiple consecutive slashes gracefully.
| #[error("transport error: {0}")] | ||
| Transport(Box<dyn std::error::Error + Send + Sync>), | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Preserve transport error on clone using Arc instead of placeholder io::Error.
Cloning to a synthetic io::Error loses the original message and source chain. Using Arc keeps content intact and restores derive(Clone).
Apply:
-#[derive(thiserror::Error, Debug)]
+#[derive(thiserror::Error, Debug, Clone)]
pub enum Error {
- #[error("transport error: {0}")]
- Transport(Box<dyn std::error::Error + Send + Sync>),
+ #[error("transport error: {0}")]
+ Transport(std::sync::Arc<dyn std::error::Error + Send + Sync>),
...
}
-impl Clone for Error {
- fn clone(&self) -> Self {
- match self {
- Error::Transport(_) => Error::Transport(Box::new(std::io::Error::other("Transport error (cloned)"))),
- ...
- }
- }
-}
+// Manual Clone impl no longer neededAnd update constructors/usages:
- Error::Transport(e.into())
+ Error::Transport(std::sync::Arc::from(e))Also applies to: 85-105
🤖 Prompt for AI Agents
In rs/moq/src/error.rs around lines 6 to 8 (and similarly for the Transport
variant usages at lines 85-105), the Transport variant currently wraps a Box<dyn
Error> and cloning falls back to a synthetic io::Error losing the original
message/source chain; change the variant to Transport(std::sync::Arc<dyn
std::error::Error + Send + Sync + 'static>) so the inner error can be shared and
the enum can derive Clone, then update all constructors and places that create
Transport to wrap the existing Box (or concrete error) in Arc::new(...) and
update any pattern matches or downcasts accordingly (e.g., when creating
Transport from errors convert them to Arc; when extracting use Arc cloning or
as_ref() without recreating io::Error).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
rs/moq-native/src/client.rs (1)
154-161: Add an HTTP client timeout for fingerprint fetch.
reqwest::gethas no default timeout; a hung endpoint will stall connection setup.- let resp = reqwest::get(fingerprint.as_str()) - .await - .context("failed to fetch fingerprint")? - .error_for_status() - .context("fingerprint request failed")?; + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .context("failed to build reqwest client")?; + let resp = client + .get(fingerprint.as_str()) + .send() + .await + .context("failed to fetch fingerprint")? + .error_for_status() + .context("fingerprint request failed")?;js/hang/src/connection.ts (1)
66-99: Fix: update status and schedule reconnect on clean close (not only on errors).If the connection closes normally,
statuscan remain "connected" and no reconnect is scheduled because the catch block isn't hit. Add a post-close transition and (optional) backoff whenreloadis true.const connection = await Promise.race([cancel, pending]); if (!connection) { pending.then((conn) => conn.close()).catch(() => {}); return; } effect.set(this.established, connection); effect.cleanup(() => connection.close()); effect.set(this.status, "connected", "disconnected"); // Reset the exponential backoff on success. this.#delay = this.delay; - await Promise.race([cancel, connection.closed()]); + await Promise.race([cancel, connection.closed()]); + // Closed (not canceled). Mark disconnected and optionally schedule a reconnect. + effect.set(this.status, "disconnected"); + if (this.reload) { + const tick = this.#tick.peek() + 1; + effect.timer(() => this.#tick.set((prev) => Math.max(prev, tick)), this.#delay); + this.#delay = Math.min(this.#delay * 2, this.maxDelay) as Time.Milli; + } + return; } catch (err) { console.warn("connection error:", err);js/hang/src/publish/audio/index.ts (1)
188-192: Defer group creation until after polyfill is ready; fix first-timestamp rotation edge case.If the polyfill load is cancelled/unsupported, the pre-created group (Lines 188-192) remains open indefinitely. Also, initializing groupTimestamp to 0 can force an immediate rotation for non-zero first timestamps.
Apply:
- let group: Moq.GroupProducer = this.#track.appendGroup(); - effect.cleanup(() => group.close()); - - let groupTimestamp = 0 as Time.Micro; + // group is created after the polyfill is ready to avoid leaking an idle group. effect.spawn(async (cancel) => { // We're using an async polyfill temporarily for Safari support. const loaded = await Promise.race([libav.polyfill(), cancel]); if (!loaded) return; // cancelled + let group: Moq.GroupProducer = this.#track.appendGroup(); + effect.cleanup(() => group.close()); + let groupTimestamp: Time.Micro | undefined; const encoder = new AudioEncoder({ output: (frame) => { if (frame.type !== "key") { throw new Error("only key frames are supported"); } - if (frame.timestamp - groupTimestamp >= Time.Micro.fromMilli(this.maxLatency)) { + if (groupTimestamp === undefined) { + groupTimestamp = frame.timestamp as Time.Micro; + } + if (frame.timestamp - groupTimestamp >= Time.Micro.fromMilli(this.maxLatency)) { group.close(); group = this.#track.appendGroup(); groupTimestamp = frame.timestamp as Time.Micro; } const buffer = Frame.encode(frame, frame.timestamp as Time.Micro); group.writeFrame(buffer);Also applies to: 193-197, 200-211
♻️ Duplicate comments (13)
rs/moq-relay/src/web.rs (6)
198-201: Good fix: optional Path for /announced prefix.Accepting
Path<Option<String>>and defaulting to""addresses the extractor failure when no suffix is provided.
96-104: Fix /certificate.sha256 handler and avoid panic when no fingerprints are present (HTTPS optional).
- Using
expect("missing certificate")contradicts “HTTPS optional”.get(fingerprint)passes aStringwhere a handler is required.Apply:
- // Get the first certificate's fingerprint. - // TODO serve all of them so we can support multiple signature algorithms. - let fingerprint = self.state.fingerprints.first().expect("missing certificate").clone(); - - let app = Router::new() - .route("/certificate.sha256", get(fingerprint)) - .route("/announced", get(serve_announced)) - .route("/announced/{*prefix}", get(serve_announced)) - .route("/fetch/{*path}", get(serve_fetch)); + // Get the first certificate's fingerprint, if any. + // TODO serve all of them so we can support multiple signature algorithms. + let fingerprint = self.state.fingerprints.first().cloned(); + + let mut app = Router::new() + .route("/announced", get(serve_announced)) + .route("/announced/{*prefix}", get(serve_announced)) + .route("/fetch/{*path}", get(serve_fetch)); + + if let Some(fp) = fingerprint { + // Serve a constant body with the fingerprint. + app = app.route("/certificate.sha256", get(move || async move { fp.clone() })); + }
150-151: Return 401 on auth failure instead of propagating internal errors (WS).Map verification errors to UNAUTHORIZED so clients get a clear 401.
- let token = state.auth.verify(&path, params.jwt.as_deref())?; + let token = state + .auth + .verify(&path, params.jwt.as_deref()) + .map_err(|_| StatusCode::UNAUTHORIZED)?;
203-204: Return 401 on auth failure instead of propagating internal errors (/announced).Consistent with WS and /fetch.
- let token = state.auth.verify(&prefix, params.jwt.as_deref())?; + let token = state + .auth + .verify(&prefix, params.jwt.as_deref()) + .map_err(|_| StatusCode::UNAUTHORIZED)?;
236-237: Return 401 on auth failure instead of propagating internal errors (/fetch).Keeps API semantics consistent.
- let token = state.auth.verify(&broadcast, params.jwt.as_deref())?; + let token = state + .auth + .verify(&broadcast, params.jwt.as_deref()) + .map_err(|_| StatusCode::UNAUTHORIZED)?;
227-233: Harden path parsing for /fetch to tolerate extra slashes and empty segments.Current split can yield empty segments (e.g., “/a//b/”), producing an empty track name. Guard and return 400 for invalid paths.
- // The path containts a broadcast/track - let mut path: Vec<&str> = path.split("/").collect(); - let track = path.pop().unwrap().to_string(); + // The path contains a broadcast/track; tolerate leading/trailing/multiple slashes. + let mut path: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); + let Some(track) = path.pop().map(|s| s.to_string()) else { + return Err(StatusCode::BAD_REQUEST.into()); + }; // We need at least a broadcast and a track. if path.is_empty() { return Err(StatusCode::BAD_REQUEST.into()); }rs/moq-native/src/client.rs (1)
188-192: Bug: matchingurl.scheme()againstmoq_lite::ALPNmakes themoqlbranch unreachable.Compare to the literal
"moql"(as done in the ALPN match above) to avoid hittingunreachable!().- let session = match url.scheme() { - "https" => web_transport_quinn::Session::connect(connection, url).await?, - moq_lite::ALPN => web_transport_quinn::Session::raw(connection, url), - _ => unreachable!(), - }; + let session = match url.scheme() { + "https" => web_transport_quinn::Session::connect(connection, url).await?, + "moql" => web_transport_quinn::Session::raw(connection, url), + _ => unreachable!(), + };#!/bin/bash # Find any other places comparing a URL scheme to an ALPN constant rg -nP 'url\.scheme\(\).*moq_lite::ALPN|match\s+url\.scheme\(\)[\s\S]*moq_lite::ALPN'rs/moq-native/src/server.rs (3)
113-119: Prevent booting without any certificatesIf neither tls-cert/ tls-key nor tls-generate yields certs, the server starts but handshakes will fail. Enforce at least one cert loaded/generated.
let fingerprints = serve.fingerprints(); + anyhow::ensure!( + !fingerprints.is_empty(), + "no TLS certificates configured; provide --tls-cert/--tls-key or --tls-generate" + );
202-209: ALPN handling inconsistent with advertised listOnly web_transport_quinn::ALPN is accepted; if moq_lite is intended, add a match arm; otherwise remove it from ALPN (see above).
120-123: Stop advertising moq_lite ALPN unless it’s actually handledALPN includes moq_lite, but accept_session rejects anything except WebTransport, causing misleading handshakes.
- tls.alpn_protocols = vec![ - web_transport_quinn::ALPN.as_bytes().to_vec(), - moq_lite::ALPN.as_bytes().to_vec(), - ]; + tls.alpn_protocols = vec![ + web_transport_quinn::ALPN.as_bytes().to_vec(), + ];rs/moq/src/error.rs (1)
8-9: Good: transport errors now Arc-based and Clone-safe.Switching to
Arc<dyn Error + Send + Sync>restoresderive(Clone)without losing error context. Matches prior guidance.rs/moq/src/session/writer.rs (1)
33-47: Addresses earlier partial-write concern via write_all.Introducing
write_all(Buf)and keepingwriteprivate prevents accidental partial writes. This resolves the prior issue.js/moq/src/connection.ts (1)
129-141: Harden fingerprint fetch: check HTTP status, trim, and validate hex.A non-200 (e.g., HTML error) or trailing newlines will currently flow into
Hex.toBytes, causing opaque failures.- // Fetch the fingerprint from the server. - const fingerprint = await Promise.race([fetch(fingerprintUrl), cancel]); - if (!fingerprint) return undefined; - - const fingerprintText = await Promise.race([fingerprint.text(), cancel]); - if (fingerprintText === undefined) return undefined; + // Fetch the fingerprint from the server. + const res = await Promise.race([fetch(fingerprintUrl), cancel]); + if (!res) return undefined; + if (!res.ok) { + console.warn(fingerprintUrl.toString(), `fingerprint fetch failed: ${res.status} ${res.statusText}`); + return undefined; + } + const fingerprintText = (await Promise.race([res.text(), cancel]))?.trim(); + if (!fingerprintText) return undefined; + // Optional: basic hex sanity check (64 hex chars for sha-256) + if (!/^[\da-fA-F]{64}$/.test(fingerprintText)) { + console.warn(fingerprintUrl.toString(), "invalid sha-256 fingerprint format"); + return undefined; + }
🧹 Nitpick comments (32)
rs/moq-relay/src/web.rs (1)
105-109: Limit WS fallback route to GET to avoid 400s on non-WS methods.
any(serve_ws)can intercept non-WS requests to unknown paths and return 400 due to extractor failure. GET is sufficient for WS handshakes.- true => app.route("/{*path}", any(serve_ws)), + true => app.route("/{*path}", get(serve_ws)),rs/moq-native/Cargo.toml (1)
16-16: Consider gatinganyhow's backtrace feature.Enabling
anyhow/backtraceglobally increases build size; make it opt-in via a crate feature.-anyhow = { version = "1", features = ["backtrace"] } +anyhow = "1"Add to this Cargo.toml (outside the shown range):
[features] default = [] backtrace = ["anyhow/backtrace"]rs/moq-native/src/client.rs (3)
169-177: De-duplicate scheme→ALPN and scheme→session logic.Derive both from a single parsed enum to prevent drift between matches.
Example (outside shown range):
enum Scheme { Https, Moql } impl TryFrom<&str> for Scheme { type Error = anyhow::Error; fn try_from(s: &str) -> anyhow::Result<Self> { match s { "https" => Ok(Scheme::Https), "moql" => Ok(Scheme::Moql), _ => anyhow::bail!("unsupported") } } }Use it to set ALPN and choose
Session::{connect,raw}.
113-118: Gate BBR with a feature or config knob.Keep it off by default, but make it easy to enable for experiments.
-//transport.congestion_controller_factory(Arc::new(quinn::congestion::BbrConfig::default())); +#[cfg(feature = "bbr")] +transport.congestion_controller_factory(Arc::new(quinn::congestion::BbrConfig::default()));Add to Cargo.toml:
[features] bbr = []
258-264: Use constant‑time compare for fingerprint equality.Minor hardening: avoid timing leakage on mismatch.
- if fingerprint.as_ref() == self.fingerprint.as_slice() { + if ring::constant_time::verify_slices_are_equal( + fingerprint.as_ref(), + self.fingerprint.as_slice() + ).is_ok() {rs/moq-native/src/server.rs (2)
38-47: Make tls-cert/tls-key accept comma-delimited lists; clarify pairing semanticsSupport comma-delimited env/CLI values for Vecs and clarify that certs/keys are paired by position.
- /// Load the given certificate from disk. + /// Load the given certificates from disk (paired with --tls-key by position). - #[arg(long = "tls-cert", id = "tls-cert", env = "MOQ_SERVER_TLS_CERT")] + #[arg(long = "tls-cert", id = "tls-cert", env = "MOQ_SERVER_TLS_CERT", value_delimiter = ',')] @@ - /// Load the given key from disk. + /// Load the given keys from disk. - #[arg(long = "tls-key", id = "tls-key", env = "MOQ_SERVER_TLS_KEY")] + #[arg(long = "tls-key", id = "tls-key", env = "MOQ_SERVER_TLS_KEY", value_delimiter = ',')]Also applies to: 50-55
228-252: Validate that the private key matches the certificateCurrently loads without verifying key/cert pairing; a mismatch will fail at handshake time. Consider checking SPKI equality before pushing CertifiedKey.
rs/moq/src/error.rs (1)
64-85: Reduce boilerplate: add a helper constructor for transport errors.This avoids repeating
map_err(|e| Error::Transport(Arc::new(e)))at call sites.impl Error { /// An integer code that is sent over the wire. pub fn to_code(&self) -> u32 { @@ } } + + /// Helper to wrap transport-layer errors uniformly. + pub fn transport<E>(e: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + Self::Transport(Arc::new(e)) + } }rs/moq/src/session/stream.rs (1)
11-20: Open path is correct; consider using the helper for error wrapping.Once
Error::transportexists, you can shorten the mapping.- let (send, recv) = session.open_bi().await.map_err(|err| Error::Transport(Arc::new(err)))?; + let (send, recv) = session.open_bi().await.map_err(Error::transport)?;rs/moq/src/session/reader.rs (2)
7-10: Make Reader crate-internal to match Writer’s visibility.
Writerispub(super)whileReaderispub. Unless external use is intended, align visibility to avoid semver surface expansion.-pub struct Reader<S: web_transport_trait::RecvStream> { +pub(super) struct Reader<S: web_transport_trait::RecvStream> {
20-44: Guard against unbounded buffer growth (optional).If peers send oversized frames or malformed data,
BytesMutcould grow without limit. Consider a max buffer cap orDecode-level size checks to fail fast.rs/moq/src/session/writer.rs (1)
65-70: Avoid resetting after a clean finish.Resetting on Drop even after
finish()can mis-signal errors. Track completion and only reset if not finished.pub(super) struct Writer<S: web_transport_trait::SendStream> { stream: S, buffer: bytes::BytesMut, + finished: bool, } @@ pub fn new(stream: S) -> Self { Self { stream, buffer: Default::default(), + finished: false, } } @@ pub async fn finish(&mut self) -> Result<(), Error> { self.stream.finish().await.map_err(|e| Error::Transport(Arc::new(e)))?; + self.finished = true; Ok(()) } @@ impl<S: web_transport_trait::SendStream> Drop for Writer<S> { fn drop(&mut self) { // Unlike the Quinn default, we abort the stream on drop. - self.stream.reset(Error::Cancel.to_code()); + if !self.finished { + self.stream.reset(Error::Cancel.to_code()); + } } }Also applies to: 49-53, 5-10, 11-17
rs/moq/src/session/publisher.rs (2)
202-276: Surface group-task errors for debugging (optional).Currently, completions of
old_group/new_groupfutures ignore theirResult. Log errors to aid diagnosis without changing behavior.- Some(_) = async { Some(old_group.as_mut()?.await) } => { + Some(res) = async { Some(old_group.as_mut()?.await) } => { + if let Err(err) = res { + tracing::debug!(subscribe = %subscribe.id, track = %track.info.name, %err, "old group task ended with error"); + } old_group = None; old_sequence = None; continue; }, - Some(_) = async { Some(new_group.as_mut()?.await) } => { + Some(res) = async { Some(new_group.as_mut()?.await) } => { + if let Err(err) = res { + tracing::debug!(subscribe = %subscribe.id, track = %track.info.name, %err, "new group task ended with error"); + } new_group = old_group; new_sequence = old_sequence; old_group = None; old_sequence = None; continue; },
263-264: Prefer debug_assert! to avoid panicking in release.Keep the check in debug builds while preventing a hard crash in production.
- assert!(old_group.is_none()); + debug_assert!(old_group.is_none());js/hang/src/frame.ts (2)
276-283: Make endianness explicit in 2‑byte varint read.DataView defaults to big‑endian, but being explicit avoids ambiguity and future regressions.
- v = view.getUint16(0) & 0x3fff; + v = view.getUint16(0, false) & 0x3fff;
17-29: Minor: simplify allocation math.
sourceandSourceboth exposebyteLength; the ternary duplicates the same value.- const data = new Uint8Array(8 + (source instanceof Uint8Array ? source.byteLength : source.byteLength)); + const data = new Uint8Array(8 + source.byteLength); ... - return data.subarray(0, (source instanceof Uint8Array ? source.byteLength : source.byteLength) + size); + return data.subarray(0, source.byteLength + size);js/justfile (4)
11-11: Prefer explicit workspace flag for installs.Use
pnpm -w install(orpnpm -w i) to make it explicit that the install is at the workspace root.- pnpm i + pnpm -w i
15-20: Deterministic CI installs and clearer intent.Minor clarity: use
-wfor workspace-root installs; optionally add--prefer-offlineto leverage CI cache.-check: - pnpm install --frozen-lockfile +check: + pnpm -w install --frozen-lockfile pnpm -r run check pnpm -r test pnpm exec biome check
23-26: Keep fix fast but consistent.Use workspace flag for install; leave lockfile unfrozen for fixes.
-fix: - pnpm install +fix: + pnpm -w install pnpm exec biome check --fix
34-37: Align build with CI determinism.Same note: workspace flag improves intent/readability.
-build: - pnpm install --frozen-lockfile +build: + pnpm -w install --frozen-lockfile pnpm -r run buildjs/moq/src/connection.ts (3)
1-1: Consider dynamic import for the WS polyfill to trim bundle size.Static import ships the fallback to all users. Lazy-load only if/when the fallback path is taken.
Example approach:
// lazy: only load when needed async function connectWebSocket(url: URL, cancel: Promise<void>): Promise<WebTransport | undefined> { const { default: WebTransportWs } = await import("@kixelated/web-transport-ws"); const quic = new WebTransportWs(url); // ... rest unchanged }
46-53: Nit: feature-detect viatypeoffor consistency.Minor consistency/readability tweak.
- const webtransport = globalThis.WebTransport ? connectWebTransport(url, cancel, props?.webtransport) : undefined; + const webtransport = + typeof globalThis.WebTransport !== "undefined" + ? connectWebTransport(url, cancel, props?.webtransport) + : undefined;
52-59: Tiny cleanup: delay promise type.Avoid declaring a generic you don’t resolve; keeps types exact.
- ? new Promise<WebTransport>((resolve) => setTimeout(resolve, headstart)).then(() => { + ? new Promise<void>((resolve) => setTimeout(resolve, headstart)).then(() => {js/hang/src/connection.ts (1)
88-96: Optional: add jitter to backoff to avoid synchronized reconnects.A small randomization reduces thundering herd when many clients reconnect simultaneously.
- this.#delay = Math.min(this.#delay * 2, this.maxDelay) as Time.Milli; + const jitter = 0.8 + Math.random() * 0.4; // 0.8x–1.2x + this.#delay = Math.min(this.#delay * 2 * jitter, this.maxDelay) as Time.Milli;js/hang/src/watch/audio/index.ts (3)
18-18: Nit: probable typo — "JITTER_UNDERHEAD" → "JITTER_OVERHEAD".Purely cosmetic; makes the intent clearer.
-const JITTER_UNDERHEAD = 25 as Time.Milli; +const JITTER_OVERHEAD = 25 as Time.Milli;And update its single usage accordingly.
157-166: Harden init: handle polyfill/configure failures and ensure decoder flushes on teardown.Avoid unhandled rejections if the polyfill fails or
configurethrows, and flush buffered frames on close.- effect.spawn(async (cancel) => { - const loaded = await Promise.race([libav.polyfill(), cancel]); - if (!loaded) return; // cancelled - - const decoder = new AudioDecoder({ + effect.spawn(async (cancel) => { + let decoder: AudioDecoder | undefined; + try { + const loaded = await Promise.race([libav.polyfill(), cancel]); + if (!loaded) return; // cancelled + + decoder = new AudioDecoder({ output: (data) => this.#emit(data), error: (error) => console.error(error), }); - effect.cleanup(() => decoder.close()); + effect.cleanup(() => decoder?.close()); - const config = info.config; - const description = config.description ? Hex.toBytes(config.description) : undefined; + const config = info.config; + const description = config.description ? Hex.toBytes(config.description) : undefined; - decoder.configure({ - ...config, - description, - }); + // Optional: preflight support check to handle "codec present but unsupported" runtimes. + // const { supported } = await AudioDecoder.isConfigSupported({ ...config, description }); + // if (!supported) throw new Error("Opus decode not supported"); + + decoder.configure({ + ...config, + description, + }); + } catch (e) { + console.error("Audio decoder initialization failed:", e); + return; + } + effect.cleanup(() => decoder?.flush?.().catch(() => {}));Also applies to: 170-174
109-129: Optional: race worklet module load with cancellation.If the component is closed quickly, we can skip loading the module.
- effect.spawn(async () => { + effect.spawn(async (cancel) => { // Register the AudioWorklet processor - await context.audioWorklet.addModule(RenderWorklet); + const loaded = await Promise.race([context.audioWorklet.addModule(RenderWorklet), cancel]); + if (!loaded) return; // cancelledjs/hang/src/util/libav.ts (2)
5-7: Optional: presence check can be too coarse.Browsers may expose
AudioDecoderbut lack Opus support. Consider a codec-specific preflight (e.g., Opus) and only load the polyfill if unsupported.Example:
// Call site (watch/publish) before configure: const { supported } = await AudioDecoder.isConfigSupported({ codec: "opus", numberOfChannels, sampleRate, description, }); if (!supported) await libav.polyfill();
9-26: Replacerequirewith dynamicimport()and add error handling
In js/hang/src/util/libav.ts, use dynamic imports for@libav.js/variant-opusandlibavjs-webcodecs-polyfillwrapped in a try/catch to log failures and returnfalse, improving ESM and SSR compatibility.// NOTE: we use require here to avoid tsc errors with libavjs-webcodecs-polyfill. - loading = Promise.all([require("@libav.js/variant-opus"), require("libavjs-webcodecs-polyfill")]).then( - async ([opus, libav]) => { - await libav.load({ - LibAV: opus, - polyfill: true, - }); - return true; - }, - ); + loading = (async () => { + try { + const [opus, libav]: any = await Promise.all([ + import("@libav.js/variant-opus"), + import("libavjs-webcodecs-polyfill"), + ]); + await libav.load({ LibAV: opus, polyfill: true }); + return true; + } catch (err) { + console.error("Failed to load Opus polyfill:", err); + return false; + } + })();[js/hang/src/util/libav.ts:15]
js/hang/src/publish/audio/index.ts (3)
228-236: Add lightweight backpressure guard before allocating/copying.Avoids latency blowups when encodeQueue backs up.
- worklet.port.onmessage = ({ data }: { data: Capture.AudioFrame }) => { + worklet.port.onmessage = ({ data }: { data: Capture.AudioFrame }) => { + if (encoder.encodeQueueSize > 2) { + // Drop under pressure to keep latency bounded. + return; + } const channels = data.channels.slice(0, settings.channelCount); const joinedLength = channels.reduce((a, b) => a + b.length, 0); const joined = new Float32Array(joinedLength);Optional: cache and reuse a Float32Array when joinedLength is stable to reduce GC pressure.
193-197: Optional: surface polyfill readiness for observability.Consider logging/metric when the polyfill path is taken vs native, to aid debugging on Safari.
228-236: Minor: assume equal-channel lengths to avoid first reduce.If capture guarantees equal lengths per channel, you can compute joinedLength as channels.length * channels[0].length and skip one pass. Leave as-is if not guaranteed.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (2)
js/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlrs/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (38)
.github/workflows/check.yml(1 hunks)js/hang-demo/package.json(1 hunks)js/hang-demo/vite.config.ts(1 hunks)js/hang/package.json(1 hunks)js/hang/src/connection.ts(4 hunks)js/hang/src/frame.ts(1 hunks)js/hang/src/publish/audio/index.ts(2 hunks)js/hang/src/support/element.ts(2 hunks)js/hang/src/support/index.ts(3 hunks)js/hang/src/util/libav.ts(1 hunks)js/hang/src/watch/audio/index.ts(2 hunks)js/hang/src/watch/audio/render-worklet.ts(0 hunks)js/hang/src/watch/element.ts(1 hunks)js/justfile(1 hunks)js/moq-token/package.json(1 hunks)js/moq/package.json(1 hunks)js/moq/src/connection.ts(4 hunks)js/moq/src/stream.ts(2 hunks)js/package.json(1 hunks)justfile(1 hunks)rs/Cargo.toml(1 hunks)rs/justfile(2 hunks)rs/moq-native/Cargo.toml(2 hunks)rs/moq-native/src/client.rs(3 hunks)rs/moq-native/src/server.rs(5 hunks)rs/moq-relay/Cargo.toml(2 hunks)rs/moq-relay/cfg/leaf.toml(1 hunks)rs/moq-relay/src/cluster.rs(2 hunks)rs/moq-relay/src/connection.rs(1 hunks)rs/moq-relay/src/web.rs(4 hunks)rs/moq/src/coding/varint.rs(2 hunks)rs/moq/src/error.rs(2 hunks)rs/moq/src/session/mod.rs(7 hunks)rs/moq/src/session/publisher.rs(14 hunks)rs/moq/src/session/reader.rs(3 hunks)rs/moq/src/session/stream.rs(2 hunks)rs/moq/src/session/subscriber.rs(12 hunks)rs/moq/src/session/writer.rs(2 hunks)
💤 Files with no reviewable changes (1)
- js/hang/src/watch/audio/render-worklet.ts
✅ Files skipped from review due to trivial changes (3)
- js/hang/src/watch/element.ts
- js/hang-demo/package.json
- js/moq-token/package.json
🚧 Files skipped from review as they are similar to previous changes (10)
- rs/moq-relay/src/cluster.rs
- js/hang/package.json
- js/moq/src/stream.ts
- rs/moq/src/coding/varint.rs
- js/hang/src/support/element.ts
- rs/moq-relay/cfg/leaf.toml
- rs/moq-relay/Cargo.toml
- rs/moq/src/session/subscriber.rs
- rs/moq/src/session/mod.rs
- rs/moq-relay/src/connection.rs
🧰 Additional context used
📓 Path-based instructions (7)
{rs,js}/hang/**
📄 CodeRabbit inference engine (CLAUDE.md)
All media logic belongs in the hang layer (rs/hang and js/hang)
Files:
js/hang/src/util/libav.tsjs/hang/src/frame.tsjs/hang/src/watch/audio/index.tsjs/hang/src/publish/audio/index.tsjs/hang/src/connection.tsjs/hang/src/support/index.ts
js/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place JS/TS packages under the js/ directory
Files:
js/hang/src/util/libav.tsjs/hang/src/frame.tsjs/hang-demo/vite.config.tsjs/package.jsonjs/moq/package.jsonjs/hang/src/watch/audio/index.tsjs/hang/src/publish/audio/index.tsjs/hang/src/connection.tsjs/hang/src/support/index.tsjs/moq/src/connection.tsjs/justfile
justfile
📄 CodeRabbit inference engine (CLAUDE.md)
Use just as the task runner; define developer tasks in the repository justfile
Files:
justfile
rs/moq-relay/**
📄 CodeRabbit inference engine (CLAUDE.md)
The CDN/relay (rs/moq-relay) must not include application logic, media codecs, or track details
Files:
rs/moq-relay/src/web.rs
rs/**/src/**/*.rs
📄 CodeRabbit inference engine (CLAUDE.md)
Rust tests should be integrated within source files (inline unit tests)
Files:
rs/moq-relay/src/web.rsrs/moq/src/error.rsrs/moq-native/src/client.rsrs/moq/src/session/reader.rsrs/moq/src/session/stream.rsrs/moq-native/src/server.rsrs/moq/src/session/publisher.rsrs/moq/src/session/writer.rs
rs/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place Rust crates under the rs/ directory
Files:
rs/moq-relay/src/web.rsrs/moq/src/error.rsrs/moq-native/src/client.rsrs/justfilers/moq/src/session/reader.rsrs/moq-native/Cargo.tomlrs/Cargo.tomlrs/moq/src/session/stream.rsrs/moq-native/src/server.rsrs/moq/src/session/publisher.rsrs/moq/src/session/writer.rs
rs/Cargo.toml
📄 CodeRabbit inference engine (CLAUDE.md)
Maintain the Rust workspace configuration in rs/Cargo.toml
Files:
rs/Cargo.toml
🧠 Learnings (2)
📚 Learning: 2025-08-13T21:30:35.001Z
Learnt from: CR
PR: kixelated/moq#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-13T21:30:35.001Z
Learning: Applies to justfile : Use just as the task runner; define developer tasks in the repository justfile
Applied to files:
justfile
📚 Learning: 2025-08-13T21:30:35.001Z
Learnt from: CR
PR: kixelated/moq#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-13T21:30:35.001Z
Learning: Applies to rs/Cargo.toml : Maintain the Rust workspace configuration in rs/Cargo.toml
Applied to files:
rs/justfilers/Cargo.toml
🧬 Code graph analysis (11)
js/hang/src/watch/audio/index.ts (1)
js/hang/src/publish/audio/index.ts (4)
effect(106-149)effect(151-164)effect(166-255)effect(257-279)
rs/moq-relay/src/web.rs (6)
rs/moq-relay/src/cluster.rs (3)
get(96-101)new(58-67)run(103-150)rs/moq-native/src/server.rs (3)
new(86-146)accept(157-178)close(217-219)rs/moq/src/session/mod.rs (3)
new(27-74)accept(103-114)close(151-153)rs/moq-relay/src/auth.rs (1)
new(70-88)rs/moq-relay/src/connection.rs (1)
run(14-59)rs/hang-cli/src/server.rs (3)
server(14-37)publish(89-102)accept(39-64)
js/hang/src/publish/audio/index.ts (2)
js/hang/src/watch/audio/index.ts (3)
effect(85-130)effect(132-142)effect(144-194)js/hang/src/publish/audio/capture.ts (1)
AudioFrame(3-6)
js/hang/src/connection.ts (2)
js/hang/src/meet/element.ts (2)
url(166-168)url(170-172)js/hang/src/publish/element.ts (2)
url(92-94)url(96-98)
rs/moq-native/src/client.rs (1)
rs/hang-cli/src/client.rs (1)
connect(23-53)
rs/moq/src/session/reader.rs (2)
rs/moq/src/session/mod.rs (3)
new(27-74)stream(146-146)closed(156-158)rs/moq/src/session/writer.rs (2)
new(12-17)closed(59-62)
rs/moq/src/session/stream.rs (3)
rs/moq/src/session/mod.rs (2)
new(27-74)accept(103-114)rs/moq/src/session/reader.rs (1)
new(13-18)rs/moq/src/session/writer.rs (1)
new(12-17)
rs/moq-native/src/server.rs (1)
rs/moq-native/src/client.rs (3)
default(42-47)new(64-130)new(244-246)
js/moq/src/connection.ts (3)
js/hang/src/connection.ts (2)
ConnectionProps(5-24)Connection(28-104)js/moq/src/lite/connection.ts (4)
Connection(19-198)stream(127-142)stream(144-160)stream(181-189)js/moq/src/ietf/connection.ts (2)
Connection(29-264)stream(246-255)
rs/moq/src/session/publisher.rs (2)
rs/moq/src/session/writer.rs (2)
new(12-17)finish(50-53)rs/moq/src/session/stream.rs (1)
accept(22-32)
rs/moq/src/session/writer.rs (2)
rs/moq/src/session/reader.rs (1)
new(13-18)rs/moq/src/coding/encode.rs (10)
encode(6-6)encode(10-12)encode(16-18)encode(22-25)encode(29-32)encode(36-41)encode(45-50)encode(54-57)encode(61-64)encode(68-70)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Check
🔇 Additional comments (32)
rs/moq-relay/src/web.rs (1)
132-136: Confirm intended behavior when neither HTTP nor HTTPS is configured.With both listeners absent,
run()returns immediately (no servers started). If unintentional, validate config or log a warning.I can add a small guard to log/bail; let me know the desired behavior.
rs/moq-native/Cargo.toml (2)
36-36: Switch toweb-transport-quinn(workspace) looks good.Aligns with the code changes in lib/server; no issues spotted.
23-23: rcgen 0.14 bump: manual verification needed
Ensure there are no breaking changes by running locally after configuring your Rust toolchain:rustup default stable cargo update -p rcgen cargo tree -i rcgen cargo check -p moq-nativers/Cargo.toml (1)
23-25: Modular transport split (quinn/trait/ws) is sound.Good centralization via workspace.dependencies; no action needed.
rs/moq-native/src/server.rs (2)
88-93: Comment-only change — no action neededBBR remains intentionally disabled; OK.
100-107: Good: enforce 1:1 cert/key pairing and iterate safelyThe length check plus zip avoids index bugs and mismatches.
rs/moq/src/error.rs (1)
72-72: Code mapping preserved.Keeping transport errors at code 4 maintains on-wire compatibility.
rs/moq/src/session/stream.rs (2)
6-9: Generic Streamlooks solid.Tying Reader/Writer to
S::RecvStream/S::SendStreamcleanly abstracts the transport.
22-27: Accept path consistent with open.Symmetric error handling and construction; good.
rs/moq/src/session/reader.rs (1)
30-36: Decode/read/closed paths handle transport errors uniformly.
map_err(Error::Transport(Arc::new(...)))is applied consistently; semantics look right for EOF vs. short decode.Also applies to: 63-67, 71-78
rs/moq/src/session/writer.rs (1)
19-31: Encode flush loop is correct.Drains the buffer via
write_bufuntil empty; no partial-write risk.rs/moq/src/session/publisher.rs (4)
31-46: Control-stream dispatch is clear and resilient.Decoding
ControlTypethen delegating to announce/subscribe handlers keeps responsibilities separated; warnings on handler errors are appropriate.
84-136: Announce loop and graceful finish look good.Initial ANNOUNCE_INIT followed by streaming updates, and
finish()onNoneis correct.
156-158: Consistent classification for transport-layer cancellations.Treating
Error::Transport(_)similar toCancelavoids noisy logs on normal disconnects.
279-293: serve_group: transport wrapping and finish lifecycle are correct.Opening uni, setting priority, writing frames, and finishing cleanly is aligned with the new Writer/Reader APIs.
Also applies to: 327-331
js/package.json (1)
8-16: Dev tooling bumps look good.No functional impact. Ensure the workspace lockfile is regenerated with pnpm 10.15.1 to avoid CI cache mismatches.
js/hang-demo/vite.config.ts (1)
23-25: LGTM: exclude LibAV variant from pre‑bundling.This aligns with dynamic polyfill loading and avoids optimizeDeps churn.
js/moq/package.json (1)
19-25: Good: Windows‑safe test glob and WS transport dep added.
- Test script now uses double quotes (cross‑platform).
- Adding @kixelated/web-transport-ws as a runtime dependency matches the new fallback path.
rs/justfile (2)
138-141: CI check flow is clearer.Running tests first, then clippy with
-D warnings, then fmt check is a solid order.
150-152: Auto‑fix target is sensible.
clippy --fixwith staged/dirty allowed plus fmt keeps churn low for quick cleanups..github/workflows/check.yml (1)
32-32: CI invocation matches repo justfile; LGTM.
nix develop --command just checkaligns with the new parameterless check targets.justfile (2)
95-99: Root check delegation looks good.Flagless
checkdelegating tors/jsmatches the new CI entrypoint.
101-105: Root fix delegation looks good.Consistent with
check; no issues.js/hang/src/support/index.ts (1)
126-126: Confirm downstreams updated foropusstatus change.
opusswitched from boolean to a tri-state string. Please verify all call sites and UI bindings now handle "full" | "partial" | "none".js/hang/src/connection.ts (2)
21-23: WebSocket fallback plumbing looks good.Prop is surfaced with sane default, stored as readonly, and forwarded to
Moq.connect(...). Matches the PR goal.Also applies to: 49-49, 68-68
26-26: Removal of “unsupported” is safe
Verified no internal call sites in js/hang reference the removed literal.js/hang/src/publish/audio/index.ts (6)
7-7: Polyfill import placement is correct.Lives under js/hang and aligns with the “media logic in hang layer” guideline. No issues.
198-217: Encoder init and cleanup look solid.Output/err handlers wired, and encoder closed on cleanup. Good.
219-224: Encoder.configure parameters look correct for Opus.Sample rate and channel count match the capture context. No changes needed.
248-253: Message handler lifecycle is correct.Encode then close the AudioData; handler cleared on cleanup. Looks good.
226-226: Config propagation timing is appropriate.Setting #config after configure ensures the catalog isn’t published before the encoder is ready.
7-7: polyfill idempotency & failure modes verified. libav.polyfill caches its loading promise in a module-scopeloadingvariable; therequirecalls andlibav.loadexecute only on the first invocation, and all subsequent calls await the same promise without re-running heavy work. It never resolvesfalse—unsupported environments or load errors reject the promise, so calling code must handle rejections.
| const frame = new AudioData({ | ||
| format: "f32-planar", | ||
| sampleRate: worklet.context.sampleRate, | ||
| numberOfFrames: channels[0].length, | ||
| numberOfChannels: channels.length, | ||
| timestamp: data.timestamp, | ||
| data: joined, | ||
| transfer: [joined.buffer], | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Remove unsupported ‘transfer’ from AudioData init.
AudioDataInit does not accept a transfer list; some engines may ignore it, others may throw. It doesn’t zero-copy anyway.
const frame = new AudioData({
format: "f32-planar",
sampleRate: worklet.context.sampleRate,
numberOfFrames: channels[0].length,
numberOfChannels: channels.length,
timestamp: data.timestamp,
data: joined,
- transfer: [joined.buffer],
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const frame = new AudioData({ | |
| format: "f32-planar", | |
| sampleRate: worklet.context.sampleRate, | |
| numberOfFrames: channels[0].length, | |
| numberOfChannels: channels.length, | |
| timestamp: data.timestamp, | |
| data: joined, | |
| transfer: [joined.buffer], | |
| }); | |
| const frame = new AudioData({ | |
| format: "f32-planar", | |
| sampleRate: worklet.context.sampleRate, | |
| numberOfFrames: channels[0].length, | |
| numberOfChannels: channels.length, | |
| timestamp: data.timestamp, | |
| data: joined, | |
| }); |
🤖 Prompt for AI Agents
In js/hang/src/publish/audio/index.ts around lines 238 to 246, the AudioData
constructor is being passed a non-standard 'transfer' property in the init
object which AudioDataInit does not accept; remove the transfer entry from the
init object and pass only the supported fields (format, sampleRate,
numberOfFrames, numberOfChannels, timestamp, data). Ensure you do not attempt to
use a transfer list for zero-copy here—just drop the transfer key so the
AudioData is constructed with the valid properties.
| opus: Partial; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid shadowing TypeScript’s built-in Partial<T>; rename the status type.
Using the name Partial for a non-generic union shadows TS’s global Partial<T> and can cause confusing errors/import accidents.
Apply these diffs here, and add the new alias at the top of the file (see snippet below):
- opus: Partial;
+ opus: SupportLevel;Additional changes in this file within changed lines:
- webtransport: Partial;
+ webtransport: SupportLevel;- capture: Partial;
+ capture: SupportLevel;- webtransport: typeof WebTransport !== "undefined" ? "full" : "partial",
+ webtransport: typeof WebTransport !== "undefined" ? "full" : "partial",- opus: (await audioEncoderSupported("opus")) ? "full" : "partial",
+ opus: (await audioEncoderSupported("opus")) ? "full" : "partial",- opus: (await audioDecoderSupported("opus")) ? "full" : "partial",
+ opus: (await audioDecoderSupported("opus")) ? "full" : "partial",Add this alias (outside the changed hunk) to replace the conflicting name:
// Rename the status type to avoid clashing with TS's Partial<T>
export type SupportLevel = "full" | "partial" | "none";Follow-ups: update any imports/usages in the repo that referenced the exported Partial.
🤖 Prompt for AI Agents
In js/hang/src/support/index.ts around lines 8 to 9, the file defines and
exports a non-generic type named Partial which shadows TypeScript’s global
Partial<T>; rename this status type to SupportLevel and add the new alias at the
top of the file: export type SupportLevel = "full" | "partial" | "none"; then
replace the old Partial type name and its export with SupportLevel throughout
this file (including the union use at lines 8–9) and update any other files in
the repo that import or reference the exported Partial to import/use
SupportLevel instead.
| const websocketWon = new Map<URL, boolean>(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix cache key: Map is keyed by URL object identity, not by address.
Map<URL, boolean> won’t match across calls unless the same URL instance is reused. Key by a stable string (e.g., origin) instead.
-const websocketWon = new Map<URL, boolean>();
+const websocketWon = new Map<string, boolean>();And adjust usages:
-const headstart = !webtransport || websocketWon.get(url) ? 0 : 200;
+const origin = url.origin;
+const headstart = !webtransport || websocketWon.get(origin) ? 0 : 200;- websocketWon.set(url, true);
+ websocketWon.set(origin, true);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const websocketWon = new Map<URL, boolean>(); | |
| // js/moq/src/connection.ts | |
| // Use a string key (origin) rather than URL object identity | |
| const websocketWon = new Map<string, boolean>(); | |
| // …later, when computing headstart: | |
| const origin = url.origin; | |
| const headstart = !webtransport || websocketWon.get(origin) ? 0 : 200; | |
| // …and when marking a WebSocket as “won”: | |
| websocketWon.set(origin, true); |
🤖 Prompt for AI Agents
In js/moq/src/connection.ts around lines 31-32, the Map is currently keyed by
URL object identity (Map<URL, boolean>), which fails to match across calls;
change the map to Map<string, boolean> and key it with a stable string (e.g.,
url.origin or url.toString()) instead of the URL instance, and update all
get/set/delete usages to convert incoming URL arguments to that same string key
before accessing the map so lookups are consistent across different URL objects
representing the same address.
|
|
||
| serde = { version = "1", features = ["derive"] } | ||
| tokio = "1.45" | ||
| tokio = "1.47" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Tokio bump to 1.47: confirm toolchain/MSRV and dependency convergence.
Ensure the workspace uses a compatible Rust toolchain and there are no duplicate Tokio versions.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show toolchain
fd -a 'rust-toolchain*' -u | xargs -I{} sh -c 'echo --- {}; sed -n "1,200p" "{}"'
# Check Tokio graph
cargo tree -i tokio
# Build the workspace
cargo check --workspaceLength of output: 467
Specify Rust channel to match Tokio MSRV and verify build
In rust-toolchain.toml under [toolchain], add channel = "1.70.0" (minimum required by Tokio 1.47). Then run cargo tree -i tokio and cargo check --workspace to confirm no duplicate Tokio versions and a successful build.
🤖 Prompt for AI Agents
In rs/Cargo.toml around line 21 where tokio = "1.47" is declared, the project
needs a pinned Rust toolchain matching Tokio's MSRV; add a rust-toolchain.toml
file (or update existing) with [toolchain] and channel = "1.70.0". After adding
the toolchain file, run cargo tree -i tokio to ensure no duplicate Tokio
versions and then run cargo check --workspace to verify the workspace builds
successfully; if cargo tree shows conflicting tokio versions, update dependency
versions or add overrides to align them to 1.47-compatible crates.
And fix HTTPS so it's optional.
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Chores